package tman.system.peer.tman;


import common.configuration.Global;
import common.peer.PeerAddress;
import common.peer.PeerDescriptor;
import common.peer.PeerInfo;
import common.peer.ViewEntry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class Cache {
	
	private Comparator<PeerDescriptor> rankingFunction = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getMoney() * (self.getMoney() - o1.getPrice()) > o2.getMoney() * (self.getMoney() - o2.getPrice())) {
				return 1;
			} else if (o1.getMoney() * (self.getMoney() - o1.getPrice()) < o2.getMoney() * (self.getMoney() - o2.getPrice())) {
				return -1;
			} else {
				if(o1.getCost() < o2.getCost())
					return 1;
				else if (o1.getCost() > o2.getCost())
					return -1;
				else
					return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByAge = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getAge() > o2.getAge()) {
				return 1;
			} else if (o1.getAge() < o2.getAge()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByCost = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getCost() > o2.getCost()) {
				return 1;
			} else if (o1.getCost() < o2.getCost()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByMoney = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getMoney() > o2.getMoney()) {
				return 1;
			} else if (o1.getMoney() < o2.getMoney()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByNetProfit = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
//			if(o1.getPeerAddress().isSource() || o2.getPeerAddress().isSource())
//				System.out.println("testingpoint");
			float np1 = (float)self.getMoney()/o1.getCost() - (float)o1.getPrice();
			float np2 = (float)self.getMoney()/o2.getCost() - (float)o2.getPrice();
			//System.out.println(np1 + " " + np2);
			if (np1 > np2) {
				return 1;
			} else if (np1 < np2) {
				return -1;
			} else {
				if(o1.getMoney() > o2.getMoney())
					return 1;
				else if(o1.getMoney() < o2.getMoney())
					return -1;
				else
					return 0;
			}
		}
	};

//-------------------------------------------------------------------
	private final int size;
	private PeerDescriptor self;
	private PeerDescriptor parentToBeRemoved;
	private PeerDescriptor childToBeRemoved;
	private boolean costChangeFlag;
	private ArrayList<PeerDescriptor> cyclonPartners;
	private ArrayList<PeerDescriptor> tmanPartners;
	private HashMap<Integer, PeerDescriptor> fingersList;
	private ArrayList<PeerDescriptor> children;
	private ArrayList<PeerDescriptor> parents;
	private HashMap<PeerAddress, PeerDescriptor> d2e;
	private Random random;
	private int psi;
	private int ageLimit;

//-------------------------------------------------------------------
	public Cache(int size, PeerDescriptor self) {
		super();
		this.self = self;
		this.size = size;
		costChangeFlag = false;
		this.cyclonPartners = new ArrayList<PeerDescriptor>();
		this.tmanPartners = new ArrayList<PeerDescriptor>();
		this.children = new ArrayList<PeerDescriptor>();
		this.parents = new ArrayList<PeerDescriptor>();
		this.d2e = new HashMap<PeerAddress, PeerDescriptor>();
		random = new Random();
		random.setSeed(Global.seed);
		psi = 3;
		ageLimit = 2;
	}

//-------------------------------------------------------------------
	public void incrementDescriptorAges() {
		ArrayList<PeerDescriptor> nodesToBeDeleted = new ArrayList<PeerDescriptor>();
		for (PeerDescriptor entry : tmanPartners) {
			int age = entry.incrementAndGetAge();
			if(age>ageLimit)
				nodesToBeDeleted.add(entry);
		}
		for (PeerDescriptor entryToDelete: nodesToBeDeleted)
		{
			removeEntry(entryToDelete);
		}
	}

//-------------------------------------------------------------------
	public PeerAddress selectPeerToShuffleWith() {
		if (tmanPartners.isEmpty()) {
			return null;
		}
		PeerDescriptor oldestEntry = Collections.max(tmanPartners, comparatorByAge);
		if(selectViableEntries(oldestEntry).size()>0)
		{
			if(oldestEntry.getPeerAddress().isSource())
				System.out.println("testingpoint");
			removeEntry(oldestEntry);
			return oldestEntry.getPeerAddress();
		}
		else
			return null;
	}
//-------------------------------------------------------------------
//	public PeerAddress selectPeerToShuffleWith() {
//		if (tmanPartners.isEmpty()) {
//			return null;
//		}
//		Collections.sort(tmanPartners, Collections.reverseOrder(rankingFunction));
//		int limit = Math.min(psi, tmanPartners.size());
//		int r = random.nextInt(limit);
//		PeerDescriptor randomPeer = tmanPartners.get(r);
//		return randomPeer.getPeerAddress();
//	}

//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToSendAtActive(int messageSize) {
		//ArrayList<ViewEntry> randomEntries = generateRandomSample(count, selectViableEntries(new PeerDescriptor(destinationPeer)));
		
		ArrayList<PeerDescriptor> buffer = new ArrayList<PeerDescriptor>();
		
		for (PeerDescriptor descriptor : tmanPartners)
		{
			buffer.add(descriptor);
		}
		
		for (PeerDescriptor descriptor : cyclonPartners) {
			if (self.equals(descriptor)) {
				// do not keep descriptor of self
				continue;
			}
			
			//Select node to merge in our buffer
			//If node already exists, skip (and update age if necessary)
			if (d2e.containsKey(descriptor.getPeerAddress())) {
				// we already have an entry for this peer. keep the youngest one in the buffer
				PeerDescriptor entry = d2e.get(descriptor.getPeerAddress());
				if (entry.getAge() > descriptor.getAge()) {
					// we keep the lowest age descriptor
					buffer.remove(entry);
					buffer.add(descriptor);
					continue;
				} else {
					continue;
				}
			}
			//Otherwise, we simply add the node to our buffer
			buffer.add(descriptor);
		}
		
		//The buffer is essentially the highest m descriptors from the union of tmanPartners & cyclonPartners
		Collections.sort(buffer, Collections.reverseOrder(rankingFunction));
		ArrayList<PeerDescriptor> bufferToSend = new ArrayList<PeerDescriptor>();
		int limit = Math.min(messageSize, buffer.size());
		for(int i=0; i<limit; i++)
			bufferToSend.add(buffer.get(i).copyPD());
			
		return bufferToSend;
	}

//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToSendAtPassive(int messageSize)
	{
		//ArrayList<ViewEntry> randomEntries = generateRandomSample(count, selectViableEntries(new PeerDescriptor(destinationPeer)));
		
		ArrayList<PeerDescriptor> buffer = new ArrayList<PeerDescriptor>();
		
		for (PeerDescriptor descriptor : tmanPartners)
		{
			buffer.add(descriptor);
		}
		
		for (PeerDescriptor descriptor : cyclonPartners) {
			if (self.equals(descriptor)) {
				// do not keep descriptor of self
				continue;
			}
			
			//Select node to merge in our buffer
			//If node already exists, skip (and update age if necessary)
			if (d2e.containsKey(descriptor.getPeerAddress())) {
				// we already have an entry for this peer. keep the youngest one in the buffer
				PeerDescriptor entry = d2e.get(descriptor.getPeerAddress());
				if (entry.getAge() > descriptor.getAge()) {
					// we keep the lowest age descriptor
					buffer.remove(entry);
					buffer.add(descriptor);
					continue;
				} else {
					continue;
				}
			}
			//Otherwise, we simply add the node to our buffer
			buffer.add(descriptor);
		}
		
		//The buffer is essentially the highest m descriptors from the union of tmanPartners & cyclonPartners
		Collections.sort(buffer, Collections.reverseOrder(rankingFunction));
		ArrayList<PeerDescriptor> bufferToSend = new ArrayList<PeerDescriptor>();
		int limit = Math.min(messageSize, buffer.size());
		for(int i=0; i<limit; i++)
			bufferToSend.add(buffer.get(i).copyPD());
			
		return bufferToSend;
	}

//-------------------------------------------------------------------
	public void bootstrapTManView(ArrayList<PeerDescriptor> descriptors, boolean joining) {
		cyclonPartners = descriptors;
		for (PeerDescriptor descriptor : descriptors) {
			if (self.equals(descriptor)) {
				// do not keep descriptor of self
				continue;
			}
			
			//Select cyclon partners to keep in the TMan View
			//If node already exists, skip (and update age if necessary)
			if (d2e.containsKey(descriptor.getPeerAddress())) {
				// we already have an entry for this peer. keep the youngest one
				PeerDescriptor entry = d2e.get(descriptor.getPeerAddress());
				if (entry.getAge() > descriptor.getAge()) {
					// we keep the lowest age descriptor
					removeEntry(entry);
					addEntry(descriptor);
					continue;
				} else {
					continue;
				}
			}
			if (joining)
			{
				//If this is the peer's first shuffle, keep every node that's GTE to the peer's bandwidth
				if(descriptor.getMoney() >= self.getPeerAddress().getBandwidth())
				{
					if (tmanPartners.size() < size) {
						// fill an empty slot
						addEntry(descriptor);
						continue;
					}
				}
			}
			else
			{
				//if ((descriptor.getMoney() == self.getPeerAddress().getBandwidth() || descriptor.getMoney() == self.getPeerAddress().getBandwidth()+1) && self.getMoney()>descriptor.getPrice())
				if (descriptor.getMoney()>=self.getMoney() && self.getMoney()>descriptor.getPrice())
				{
					if(tmanPartners.size() < size)
					{
						addEntry(descriptor);
						continue;
					}
					
					//Replace dissimilar nodes
					PeerDescriptor m = getDisimilarPeer();
					if(m != null)
					{
						removeEntry(m);
						addEntry(descriptor);
					}
				}				
			}
		}
	}
	
//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToKeep(PeerAddress from, ArrayList<PeerDescriptor> descriptors) {
		for (PeerDescriptor descriptor : descriptors) {

			//if((self.getPeerAddress().getBandwidth() == descriptor.getMoney() || self.getPeerAddress().getBandwidth() + 1 == descriptor.getMoney()) && self.getMoney()>descriptor.getPrice())
			if (descriptor.getMoney()>=self.getMoney() && self.getMoney()>descriptor.getPrice() && descriptor.getAge()<ageLimit)
			{
				if (self.equals(descriptor)) {
					// do not keep descriptor of self
					continue;
				}
	
				if (d2e.containsKey(descriptor.getPeerAddress())) {
					// we already have an entry for this peer. keep the youngest one
					PeerDescriptor entry = d2e.get(descriptor.getPeerAddress());
					if (entry.getAge() > descriptor.getAge()) {
						// we keep the lowest age descriptor
						removeEntry(entry);
						addEntry(descriptor);
						continue;
					} else {
						continue;
					}
				}
				
				addEntry(descriptor);
			}			
		}
	selectView();
	return selectPeersToAcquire(selectCandidates());
	}
	
//-------------------------------------------------------------------
	public final boolean decideOnChild(PeerDescriptor child)
	{
//		if(self.getPeerAddress().isSource())
//			System.out.println("I'm a source node");
		if(children.size() < self.getMoney())
		{
			children.add(child);
			reCalculatePrice();
			return true;
		}
		else if(children.size() != 0)
		{
			PeerDescriptor minChild = Collections.min(children, comparatorByMoney);
			if(child.getMoney()>minChild.getMoney())
			{
				childToBeRemoved = minChild;
				children.remove(minChild);
				children.add(child);
				reCalculatePrice();
				return true;
			}
		}
		return false;
	}

//-------------------------------------------------------------------
	public final boolean stillWantParent(PeerDescriptor parent)
	{
//		if(parent.getCost()!=9999)
//			System.out.println("testpoint");
		if(parents.size()<Global.id)
		{
			parents.add(parent);
			reCalculateCost();
			return true;
		}
		else
		{
//			Collections.sort(parents, comparatorByNetProfit);
//			PeerDescriptor minParent = parents.get(parents.size()-1);
//			float minNP = (float)self.getMoney()/minParent.getCost() - (float)minParent.getPrice();
//			float parentNP = (float)self.getMoney()/parent.getCost() - (float)parent.getPrice();
			
			//I find it more logical to compare "already acquired" parents using cost instead of net profit
			//If you want to revert back, uncomment the lines above and comment the line below
			PeerDescriptor minParent = Collections.max(parents, comparatorByCost);
			//System.out.println(minNP);
			if(parent.getCost()<minParent.getCost() || (parent.getCost() == minParent.getCost() && parent.getMoney() > minParent.getMoney()))
			{
				parentToBeRemoved = minParent;
				parents.remove(minParent);
				parents.add(parent);
				reCalculateCost();
				return true;
			}
			return false;
		}
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getAllSimilarEntries() {
		ArrayList<PeerDescriptor> descriptors = new ArrayList<PeerDescriptor>();

		for (PeerDescriptor entry : tmanPartners)
			descriptors.add(entry);
		
		return descriptors;
	}
	
//-------------------------------------------------------------------
	public final HashMap<Integer, PeerDescriptor> getAllFingers() {
		return fingersList;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getAllCyclonPartners() {
		return cyclonPartners;
	}
	
//-------------------------------------------------------------------
	public final PeerDescriptor getSelf() {
		return self;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getChildren() {
		return children;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getParents() {
		return parents;
	}
//-------------------------------------------------------------------
	public final void removeChild(PeerDescriptor child) {
		children.remove(child);
		reCalculatePrice();
	}
		
//-------------------------------------------------------------------
	public final void removeParent(PeerDescriptor parent) {
		parents.remove(parent);
		reCalculateCost();
	}
	
//-------------------------------------------------------------------
	public final void updateParent(PeerDescriptor parent) {
		int count=0;
		while(parents.contains(parent))
		{
			count++;
			parents.remove(parent);
		}
		if(count>0)
		{
			for(int i=0; i<count; i++)
			{
				parents.add(parent);
			}
			reCalculateCost();
		}
	}

//-------------------------------------------------------------------
	public boolean hasCostChanged()
	{
		if(costChangeFlag == true)
		{
			costChangeFlag = false;
			return true;
		}
		else
			return costChangeFlag;
	}

//-------------------------------------------------------------------
	public PeerAddress getChildToBeRemoved()
	{
		if(childToBeRemoved == null)
			return null;
		PeerAddress c = childToBeRemoved.getPeerAddress();
		childToBeRemoved = null;
		return c;
	}
	
//-------------------------------------------------------------------
	public PeerAddress getParentToBeRemoved()
	{
		if(parentToBeRemoved == null)
			return null;
		PeerAddress p = parentToBeRemoved.getPeerAddress();
		parentToBeRemoved = null;
		return p;
	}
	
//-------------------------------------------------------------------
    public float getAverageSimilarAge()
    {
    	Float total = (float) 0;
    	for(PeerDescriptor pd : tmanPartners)
    	{
    		total = total + pd.getAge();
    	}
    	if(tmanPartners.size() != 0)
    		return total/tmanPartners.size();
    	else
    		return (float) 0;
    }
    
//-------------------------------------------------------------------
    public float getAverageCyclonAge()
    {
    	Float total = (float) 0;
    	for(PeerDescriptor pd : cyclonPartners)
    	{
    		total = total + pd.getAge();
    	}
    	if(cyclonPartners.size() != 0)
    		return total/cyclonPartners.size();
    	else
    		return (float) 0;
    }

//-------------------------------------------------------------------
	private void addEntry(PeerDescriptor entry) {
		if(self.getPeerAddress() == entry.getPeerAddress())
		{
			System.out.println("something is wrong here");
		}
		tmanPartners.add(entry);
		d2e.put(entry.getPeerAddress(), entry);
		checkSize();
	}

//-------------------------------------------------------------------
	private void removeEntry(PeerDescriptor entry) {
		int viewSizeBefore = tmanPartners.size();
		tmanPartners.remove(entry);
		int viewSizeAfter = tmanPartners.size();
		if(viewSizeBefore == viewSizeAfter)
			System.out.println("Peer " + self.getPeerAddress() + " cannot delete parent " + entry.getPeerAddress());
		d2e.remove(entry.getPeerAddress());
		checkSize();
	}
	
//-------------------------------------------------------------------
	private PeerDescriptor getDisimilarPeer()
	{
		for(PeerDescriptor descriptor: tmanPartners)
		{
			if(!(descriptor.getMoney()>=self.getMoney() && self.getMoney()>descriptor.getPrice()))
				return descriptor;
		}
		return null;
	}
	
//-------------------------------------------------------------------
	private ArrayList<PeerDescriptor> selectCandidates()
	{
		ArrayList<PeerDescriptor> candidates = new ArrayList<PeerDescriptor>();
		float maxCost;
		if(parents.size()<Global.id)
			maxCost = 10000;
		else
			maxCost = Collections.max(parents, comparatorByCost).getCost();
		for(PeerDescriptor entry: tmanPartners)
		{
			//You might want to change max cost to min bandwidth
			if(entry.getCost()<maxCost && entry.getPrice()<self.getMoney() && !children.contains(entry) && entry.getMoney()!=0)
				candidates.add(entry);
		}
		return candidates;
	}
	
//-------------------------------------------------------------------
	private ArrayList<PeerDescriptor> selectPeersToAcquire(ArrayList<PeerDescriptor> candidates)
	{
		ArrayList<PeerDescriptor> peersToAcquire = new ArrayList<PeerDescriptor>();
		Collections.sort(candidates, Collections.reverseOrder(rankingFunction));
		//For now we'll only attempt to acquire 1 peer per shuffle
		if(candidates.size() != 0)
		{
			peersToAcquire.add(candidates.get(0));
			//We'll also remove all peers that we attempt to acquire (to prevent retries for the same peer)
			removeEntry(candidates.get(0));
		}
		return peersToAcquire;
	}
	
//-------------------------------------------------------------------
	private void reCalculateCost()
	{
		//This part might need to be revised
		float oldCost = self.getCost();
		int oldSp = self.getSP();
		int diff = Global.id - parents.size();
		float newCost = 0;
		int newSp = 9999;
		for(PeerDescriptor p: parents)
		{
			newCost += p.getSP();
			if(newSp>p.getSP())
				newSp = p.getSP();
		}
		newCost += 9999*diff;
		newCost = newCost/(float)Global.id;
		
		if(newCost != 9999)
			newCost++;
		
		if(newSp != 9999)
			newSp++;
		
		if(newCost != oldCost || newSp != oldSp)
		{
			costChangeFlag = true;
			self.setCost(newCost);
			self.setSP(newSp);
		}
	}
	
//-------------------------------------------------------------------
	private void reCalculatePrice()
	{
		if(self.getMoney()>children.size())
			self.setPrice(-1);
		else
		{
			int min=9999;
			for(PeerDescriptor c: children)
			{
				if(c.getMoney()<min)
					min= c.getMoney();
			}
			self.setPrice(min);
		}
		
	}
	
	//-------------------------------------------------------------------
	private void selectView()
	{
		int sizeDiff = tmanPartners.size() - size;
		if(sizeDiff>0)
		{
			Collections.sort(tmanPartners, Collections.reverseOrder(rankingFunction));
			for(int i=0; i<sizeDiff; i++)
			{
				PeerDescriptor peerToBeRemoved = tmanPartners.get(tmanPartners.size()-1);
				removeEntry(peerToBeRemoved);
			}
		}
	}
	
	//-------------------------------------------------------------------
	
	//Select entries that can be sent to a certain peer
	private ArrayList<PeerDescriptor> selectViableEntries(PeerDescriptor randomPeer)
	{
		ArrayList<PeerDescriptor> viableEntries = new ArrayList<PeerDescriptor>();
		for(PeerDescriptor entry: tmanPartners)
		{
			if(!randomPeer.equals(entry))
			{
				if(entry.getMoney()==randomPeer.getMoney() || entry.getMoney()==randomPeer.getMoney()+1)
					viableEntries.add(entry);
			}
		}
		return viableEntries;
	}

//-----------------------------------------------------------------------
	private void checkSize() {
		if (tmanPartners.size() != d2e.size())
		{
			for(PeerDescriptor entry: tmanPartners)
			{
				System.out.println("Entry Address: " + entry.getPeerAddress());
			}
			for(PeerAddress address: d2e.keySet())
			{
				System.out.println("d2e Address: " + address);
			}
			throw new RuntimeException("WHD " + tmanPartners.size() + " <> " + d2e.size());
		}
	}
}
